﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using QQ2564874169.Core;
using QQ2564874169.Core.Utils;

namespace QQ2564874169.RelationalSql
{
    public class DbExecuteMonitor
    {
        public event EventHandler<DbExecuteMonitorEventArgs> Before;
        public event EventHandler<DbExecuteMonitorEventArgs> After;
        public event EventHandler<DbExecuteMonitorErrorEventArgs> Error;
        public Hashtable Data { get; }
        public bool IsOpenPersist { get; set; }
        /// <summary>
        /// After接口触发延迟，如果为0则立刻触发（毫秒）
        /// </summary>
        public int AfterDelay { get; set; }

        private static SettingParam[] _params;
        private readonly string _beforeKey = Guid.NewGuid().ToString();
        private readonly string _afterKey = Guid.NewGuid().ToString();
        private readonly string _transactionKey = Guid.NewGuid().ToString();
        private Dictionary<string, List<ServiceSetting>> _settings = new Dictionary<string, List<ServiceSetting>>();
        private List<Type> _allbeforeServices = new List<Type>();
        private List<Type> _allafterServices = new List<Type>();

        private ConcurrentDictionary<string, SettingSummary> _settQuery =
            new ConcurrentDictionary<string, SettingSummary>();

        private Queue<ExecuteParam> _afterqueue = new Queue<ExecuteParam>();
        private ITypeContainer _container;
        private object _maxlock = new object();
        private int _max;
        private int _curr;
        private bool? _stoping;

        static DbExecuteMonitor()
        {
            _params = new[]
            {
                new SettingParam
                {
                    Operation = DbExecuteOperation.Insert,
                    AfterType = typeof(IWatchInsertAfter<>),
                    BeforeType = typeof(IWatchInsertBefore<>)
                },
                new SettingParam
                {
                    Operation = DbExecuteOperation.Delete,
                    AfterType = typeof(IWatchDeleteAfter<>),
                    BeforeType = typeof(IWatchDeleteBefore<>)
                },
                new SettingParam
                {
                    Operation = DbExecuteOperation.Update,
                    AfterType = typeof(IWatchUpdateAfter<>),
                    BeforeType = typeof(IWatchUpdateBefore<>)
                },
                new SettingParam
                {
                    Operation = DbExecuteOperation.SetNull,
                    AfterType = typeof(IWatchSetNullAfter<>),
                    BeforeType = typeof(IWatchSetNullBefore<>)
                },
                new SettingParam
                {
                    Operation = DbExecuteOperation.Proc,
                    AfterType = typeof(IWatchProcAfter<>),
                    BeforeType = typeof(IWatchProcBefore<>)
                }
            };
        }

        public DbExecuteMonitor(ITypeContainer container)
        {
            if (container == null)
                throw new ArgumentNullException(nameof(container));
            Data = new Hashtable();
            AfterDelay = 1000;
            _container = container;
        }

        protected virtual void OnBefore(DbExecuteMonitorEventArgs e)
        {
            Before?.Invoke(this, e);
        }

        protected virtual void OnAfter(DbExecuteMonitorEventArgs e)
        {
            After?.Invoke(this, e);
        }

        protected virtual void OnError(DbExecuteMonitorErrorEventArgs e)
        {
            Error?.Invoke(this, e);
        }

        public void LoadService(params Type[] types)
        {
            foreach (var type in types)
            {
                if (!type.IsClass || type.IsAbstract)
                    return;
                var ints = type.GetInterfaces().ToArray();
                if (ints.Length < 1)
                    return;
                if (typeof(IWatchBefore).IsAssignableFrom(type))
                    _allbeforeServices.Add(type);
                if (typeof(IWatchAfter).IsAssignableFrom(type))
                    _allafterServices.Add(type);
                var its = ints.Where(i => i.IsGenericType).ToArray();
                if (its.Length < 1)
                    return;

                foreach (var it in its)
                {
                    var gt = it.GetGenericTypeDefinition();
                    var bp = _params.FirstOrDefault(i => i.BeforeType.IsAssignableFrom(gt));
                    var ap = _params.FirstOrDefault(i => i.AfterType.IsAssignableFrom(gt));

                    if (ap == null && bp == null)
                        continue;

                    var table = it.GetGenericArguments().First().FullName;
                    if (string.IsNullOrEmpty(table))
                        continue;
                    if (_settings.ContainsKey(table) == false)
                    {
                        _settings.Add(table, new List<ServiceSetting>());
                    }
                    var setts = _settings[table];
                    var sett = setts.FirstOrDefault(i => i.Service == type);
                    if (sett == null)
                    {
                        sett = new ServiceSetting { Service = type };
                        setts.Add(sett);
                    }
                    if (bp != null)
                    {
                        switch (bp.Operation)
                        {
                            case DbExecuteOperation.Insert:
                                sett.IsInsertBefore = true;
                                break;
                            case DbExecuteOperation.Delete:
                                sett.IsDeleteBefore = true;
                                break;
                            case DbExecuteOperation.Update:
                                sett.IsUpdateBefore = true;
                                break;
                            case DbExecuteOperation.SetNull:
                                sett.IsSetNullBefore = true;
                                break;
                            case DbExecuteOperation.Proc:
                                sett.IsProcBefore = true;
                                break;
                        }
                    }
                    if (ap != null)
                    {
                        switch (ap.Operation)
                        {
                            case DbExecuteOperation.Insert:
                                sett.IsInsertBefore = false;
                                break;
                            case DbExecuteOperation.Delete:
                                sett.IsDeleteBefore = false;
                                break;
                            case DbExecuteOperation.Update:
                                sett.IsUpdateBefore = false;
                                break;
                            case DbExecuteOperation.SetNull:
                                sett.IsSetNullBefore = false;
                                break;
                            case DbExecuteOperation.Proc:
                                sett.IsProcBefore = false;
                                break;
                        }
                    }
                    _settQuery.AddOrUpdate(table,
                        SettingsSummary(_settings[table]),
                        (tab, value) => SettingsSummary(_settings[table]));
                }
            }
        }

        public void Close()
        {
            DbExecute.Created -= DbExecute_Created;
            _stoping = true;
            while (_stoping != null)
            {
                TaskHelper.Delay(100).Wait();
            }
            _max = 0;
            _container.Dispose();
            _container = null;
        }

        public void Start(int paralleMax = 10)
        {
            if (_max > 0)
                return;
            if (_container == null)
                throw new InvalidOperationException("该实例已经关闭，不能再次启动。");

            _max = paralleMax;

            ExecuteAfter();
            LoadPersistData();

            DbExecute.Created += DbExecute_Created;
        }

        private void LoadPersistData()
        {
            if (IsOpenPersist == false)
                return;

            TaskHelper.Run(() =>
            {
                using (var container = _container.NewContainer())
                {
                    var persister = container.Resolve<IPersister>();
                    var data = persister.Load();
                    if (data == null) return;

                    var param = data.Select(i => new ExecuteParam
                    {
                        Context = new ServiceContext
                        {
                            Model = i.Model,
                            Operation = i.Operation,
                            TableType = i.TableType
                        },
                        Setting = new ServiceSetting
                        {
                            Service = i.ServiceType
                        },
                        IsWatchBefore = false,
                        Persist = i
                    });
                    lock (_afterqueue)
                    {
                        foreach (var item in param)
                        {
                            _afterqueue.Enqueue(item);
                        }
                    }
                }
            });
        }

        private void ExecuteAfter()
        {
            if (_stoping == true)
                return;

            TaskHelper.Run(() =>
            {
                while (_stoping == null || _afterqueue.Count > 0)
                {
                    if (_stoping.HasValue && IsOpenPersist)
                        break;
                    if (AfterDelay < 1)
                    {
                        TaskHelper.Delay(1000).Wait();
                        continue;
                    }
                    ExecuteParam[] param;
                    lock (_afterqueue)
                    {
                        param = _afterqueue.ToArray();
                        _afterqueue.Clear();
                    }
                    if (param.Length > 0)
                    {
                        foreach (var item in param)
                        {
                            while (true)
                            {
                                lock (_maxlock)
                                {
                                    if (_curr == _max)
                                    {
                                        TaskHelper.Delay(10).Wait();
                                        continue;
                                    }
                                    _curr++;
                                    break;
                                }
                            }
                            TaskHelper.Run(() =>
                            {
                                OnExecute(item);
                                lock (_maxlock)
                                {
                                    _curr--;
                                }
                            });
                        }
                    }
                    else
                    {
                        TaskHelper.Delay(AfterDelay).Wait();
                    }
                }
                _stoping = null;

            }).ContinueWith(t =>
            {
                ExecuteAfter();
            });
        }

        private void DbExecute_Created(DbExecute execute)
        {
            execute.Before += Execute_Before;
            execute.After += Execute_After;
        }

        private void Execute_Before(object sender, DbExecuteBeforeEventArgs e)
        {
            if (string.IsNullOrEmpty(e.TableType.FullName))
                return;

            if (_allbeforeServices.Count < 1)
            {
                var tname = e.TableType.FullName;

                if (_settQuery.ContainsKey(tname) == false ||
                    _settQuery[tname] == SettingSummary.Nothing ||
                    _settQuery[tname] == SettingSummary.After)
                    return;
            }

            var sql = e.Sql;
            if (sql.Data.Contains(_beforeKey) == false)
            {
                sql.TransactionBefore += Sql_TransactionBefore;
                sql.Data.Add(_beforeKey, new List<ServiceContext>());
            }
            ((List<ServiceContext>)sql.Data[_beforeKey]).Add(new ServiceContext
            {
                Model = e.Model,
                Operation = e.Operation,
                TableType = e.TableType,
                Sql = sql
            });
            var exec = (DbExecute)sender;
            if (exec.InTransaction == false)
            {
                exec.Data.Add(_transactionKey, exec.BeginTransaction());
            }
        }

        private void Execute_After(object sender, DbExecuteAfterEventArgs e)
        {
            var exec = (DbExecute)sender;

            if (exec.InTransaction && exec.Data.Contains(_transactionKey))
            {
                using (var tscope = (ITransactionScope)exec.Data[_transactionKey])
                {
                    tscope.Commit();
                    if (exec.InTransaction == false)
                        exec.Data.Remove(_transactionKey);
                }
            }
            var sql = e.Sql;
            var context = new ServiceContext
            {
                Model = e.Model,
                Operation = e.Operation,
                TableType = e.TableType,
                Sql = sql
            };
            if (exec.InTransaction)
            {
                if (sql.Data.Contains(_afterKey) == false)
                {
                    sql.Data.Add(_afterKey, new List<ServiceContext>());
                    sql.TransactionAfter += Sql_TransactionAfter;
                }
                ((List<ServiceContext>)sql.Data[_afterKey]).Add(context);
            }
            else
            {
                ExecuteAfterContext(new[] { context });
            }
        }

        private void Sql_TransactionBefore(object sender, TransactionFinishEventArgs e)
        {
            var sql = (ISql)sender;
            if (sql.Data.Contains(_beforeKey) == false)
                return;

            sql.TransactionBefore -= Sql_TransactionBefore;

            if (!e.IsCommited)
            {
                sql.Data.Remove(_beforeKey);
                return;
            }
            while (sql.Data.Contains(_beforeKey))
            {
                var contexts = new List<ServiceContext>();
                contexts.AddRange((List<ServiceContext>)sql.Data[_beforeKey]);
                sql.Data.Remove(_beforeKey);
                if (contexts.Any() == false)
                    break;

                contexts.Reverse();
                var execs = GetWatchAllParam(contexts.ToArray(), true);
                execs.AddRange(ConvertToParam(contexts, true));

                foreach (var exec in execs)
                {
                    OnExecute(exec);
                }
            }
        }

        private void Sql_TransactionAfter(object sender, TransactionFinishEventArgs e)
        {
            var sql = (ISql)sender;
            if (sql.Data.Contains(_afterKey) == false)
                return;

            var contexts = (List<ServiceContext>)sql.Data[_afterKey];
            sql.Data.Remove(_afterKey);
            sql.TransactionAfter -= Sql_TransactionAfter;

            if (e.IsCommited)
            {
                ExecuteAfterContext(contexts.ToArray());
            }
        }

        private void OnExecute(ExecuteParam param)
        {
            var method = param.GetMethod();
            var mp = new[] { param.Context.Model };

            if (param.IsAll)
            {
                mp = new[] { param.Context.Operation, param.Context.TableType, param.Context.Model };
            }
            else
            {
                var args = param.Context.Model as DbUpdateEventArgs;
                if (args != null)
                {
                    mp = new[] { args.Sets, args.Where };
                }
            }

            using (var container = param.IsWatchBefore
                ? _container.NewContainer(new ServiceParam
                {
                    Instance = param.Context.Sql,
                    ServiceType = typeof(ISql),
                    Scope = ServiceScope.Single,
                    IsExternallyOwned = true
                })
                : _container.NewContainer())
            {
                var mea = new DbExecuteMonitorEventArgs
                {
                    IsBeforeHandler = param.IsWatchBefore,
                    Model = param.Context.Model,
                    Operation = param.Context.Operation,
                    TableType = param.Context.TableType,
                    Service = container.Resolve(param.Setting.Service)
                };
                OnBefore(mea);
                try
                {
                    method.Invoke(mea.Service, mp);
                }
                catch (Exception ex)
                {
                    OnError(new DbExecuteMonitorErrorEventArgs
                    {
                        Error = ex,
                        Model = param.Context.Model,
                        Operation = param.Context.Operation,
                        TableType = param.Context.TableType,
                        ServiceType = param.Setting.Service
                    });
                    if (param.IsWatchBefore)
                        throw;
                }
                if (IsOpenPersist && param.Persist != null)
                {
                    var persiter = _container.Resolve<IPersister>();
                    persiter.Remove(param.Persist);
                }
                OnAfter(mea);
            }
        }

        private void ExecuteAfterContext(ServiceContext[] contexts)
        {
            if (_max < 1) return;

            var execs = GetWatchAllParam(contexts, false);
            execs.AddRange(ConvertToParam(contexts, false).ToArray());

            if (execs.Count < 1) return;

            if (IsOpenPersist)
            {
                using (var container = _container.NewContainer())
                {
                    var persiter = container.Resolve<IPersister>();
                    persiter.Save(execs.Select(i => i.Persist = new PersistData
                    {
                        Id = Guid.NewGuid(),
                        CreateTime = DateTime.Now.Ticks,
                        Model = i.Context.Model,
                        Operation = i.Context.Operation,
                        ServiceType = i.Setting.Service,
                        TableType = i.Context.TableType
                    }));
                }
            }

            if (AfterDelay < 1)
            {
                Parallel.ForEach(execs, OnExecute);
                return;
            }

            lock (_afterqueue)
            {
                foreach (var item in execs)
                {
                    _afterqueue.Enqueue(item);
                }
            }
        }

        private List<ExecuteParam> ConvertToParam(IEnumerable<ServiceContext> contexts, bool before)
        {
            return contexts.Where(c => c.TableType.FullName != null && _settings.ContainsKey(c.TableType.FullName))
                .SelectMany(c =>
                    _settings[c.TableType.FullName].Where(i =>
                        (i.IsInsertBefore == before && c.Operation == DbExecuteOperation.Insert) ||
                        (i.IsDeleteBefore == before && c.Operation == DbExecuteOperation.Delete) ||
                        (i.IsUpdateBefore == before && c.Operation == DbExecuteOperation.Update) ||
                        (i.IsSetNullBefore == before && c.Operation == DbExecuteOperation.SetNull) ||
                        (i.IsProcBefore == before && c.Operation == DbExecuteOperation.Proc)).Select(
                        s => new ExecuteParam
                        {
                            Context = c,
                            Setting = s,
                            IsWatchBefore = before
                        }))
                .ToList();
        }

        private List<ExecuteParam> GetWatchAllParam(ServiceContext[] contexts, bool isWatchBefore)
        {
            var list = new List<ExecuteParam>();
            var services = isWatchBefore ? _allbeforeServices : _allafterServices;

            foreach (var bs in services)
            {
                foreach (var context in contexts)
                {
                    list.Add(new ExecuteParam
                    {
                        Setting = new ServiceSetting
                        {
                            Service = bs
                        },
                        Context = new ServiceContext
                        {
                            Sql = context.Sql,
                            Model = context.Model,
                            Operation = context.Operation,
                            TableType = context.TableType
                        },
                        IsAll = true,
                        IsWatchBefore = isWatchBefore
                    });
                }
            }
            return list;
        }

        private static SettingSummary SettingsSummary(IEnumerable<ServiceSetting> settings)
        {
            var list = settings.Select(i => i.GetSummary()).ToArray();
            if (list.Any(i => i == SettingSummary.BeforeAndAfter))
                return SettingSummary.BeforeAndAfter;
            if (list.Any(i => i == SettingSummary.After) && list.Any(i => i == SettingSummary.Before))
                return SettingSummary.BeforeAndAfter;
            if (list.All(i => i == SettingSummary.Before))
                return SettingSummary.Before;
            if (list.All(i => i == SettingSummary.After))
                return SettingSummary.After;
            return SettingSummary.Nothing;
        }

        private enum SettingSummary
        {
            Nothing,
            Before,
            After,
            BeforeAndAfter
        }

        private class ServiceSetting
        {
            public Type Service { get; set; }
            public bool? IsInsertBefore { get; set; }
            public bool? IsUpdateBefore { get; set; }
            public bool? IsSetNullBefore { get; set; }
            public bool? IsDeleteBefore { get; set; }
            public bool? IsProcBefore { get; set; }

            public SettingSummary GetSummary()
            {
                bool? before = null, after = null;
                if (IsDeleteBefore.HasValue)
                {
                    if (IsDeleteBefore.Value)
                    {
                        before = true;
                    }
                    else
                    {
                        after = true;
                    }
                }
                if (IsInsertBefore.HasValue)
                {
                    if (IsInsertBefore.Value)
                    {
                        before = true;
                    }
                    else
                    {
                        after = true;
                    }
                }
                if (IsProcBefore.HasValue)
                {
                    if (IsProcBefore.Value)
                    {
                        before = true;
                    }
                    else
                    {
                        after = true;
                    }
                }
                if (IsSetNullBefore.HasValue)
                {
                    if (IsSetNullBefore.Value)
                    {
                        before = true;
                    }
                    else
                    {
                        after = true;
                    }
                }
                if (IsUpdateBefore.HasValue)
                {
                    if (IsUpdateBefore.Value)
                    {
                        before = true;
                    }
                    else
                    {
                        after = true;
                    }
                }
                if (before.HasValue && after.HasValue)
                    return SettingSummary.BeforeAndAfter;
                if (before.HasValue)
                    return SettingSummary.Before;
                if (after.HasValue)
                    return SettingSummary.After;
                return SettingSummary.Nothing;
            }
        }

        private class ServiceContext
        {
            public DbExecuteOperation Operation { get; internal set; }
            public object Model { get; internal set; }
            public Type TableType { get; internal set; }
            internal ISql Sql { get; set; }
        }

        private class ExecuteParam
        {
            public bool IsWatchBefore { get; set; }
            public ServiceSetting Setting { get; set; }
            public ServiceContext Context { get; set; }
            public PersistData Persist { get; set; }
            public bool IsAll { get; set; }

            public MethodInfo GetMethod()
            {
                if (IsAll)
                {
                    if (IsWatchBefore)
                    {
                        return typeof(IWatchBefore).GetMethods().First();
                    }
                    return typeof(IWatchAfter).GetMethods().First();
                }
                var sp = _params.First(i => i.Operation == Context.Operation);
                var method = (IsWatchBefore ? sp.BeforeType : sp.AfterType).
                    MakeGenericType(Context.TableType).GetMethods().First();
                return method;
            }
        }

        private class SettingParam
        {
            public DbExecuteOperation Operation { get; set; }
            public Type BeforeType { get; set; }
            public Type AfterType { get; set; }
        }
    }
}
